﻿using System;
using System.Collections.Generic;
using System.Linq;
using log4net;
using Microsoft.SharePoint;
using System.Collections.Specialized;

namespace SharePoint.Orm.Helpers {
	/// <summary>
	/// Collection of methods for working with <see cref="SPList"/>.
	/// </summary>
	public static class SPListHelper {
		private static readonly ILog _log = LogManager.GetLogger(typeof(SPListHelper));

		/// <summary>
		/// Finds a field by title, internal name, or static name. Or, return null.
		/// </summary>
		/// <param name="list">The list to find the field on.</param>
		/// <param name="fieldName">The title, internal name, or static name of the field to find.</param>
		/// <returns>The field or null if not found.</returns>
		public static SPField FindFieldOrDefault(this SPList list, string fieldName) {
			return list.Fields.Cast<SPField>().FirstOrDefault(f => string.Equals(f.Title, fieldName, StringComparison.InvariantCultureIgnoreCase)
				|| string.Equals(f.InternalName, fieldName, StringComparison.InvariantCultureIgnoreCase)
				|| string.Equals(f.StaticName, fieldName, StringComparison.InvariantCultureIgnoreCase));
		}

		/// <summary>
		/// Find or add a field to a <see cref="SPList"/>.
		/// </summary>
		/// <param name="list">The list to find or add the field to.</param>
		/// <param name="title">The title of the field to find or add.</param>
		/// <param name="type">The type of the field if the field is not found.</param>
		/// <param name="required">True, if the field is required.</param>
		/// <returns>The field that was found or added.</returns>
		public static SPField FindOrAddField(SPList list, string title, SPFieldType type, bool required) {
			SPField field = FindFieldOrDefault(list, title);
			if (field == null) {
				string name = list.Fields.Add(title, type, required);
				field = list.Fields[name];
			}
			field.Type = type;
			field.Required = required;
			field.Update();
			return field;
		}

		/// <summary>
		/// Finds or add a field to a <see cref="SPList"/>.
		/// </summary>
		/// <param name="list">The list to find or add the field to</param>
		/// <param name="title">The title of the field to find or add</param>
		/// <param name="typeName">The name of the type</param>
		/// <param name="required">True, if the field is required</param>
		/// <returns>The field that was found or added</returns>
		public static SPField FindOrAddField(SPList list, string title, string typeName, bool required) {
			SPField field = FindFieldOrDefault(list, title);
			if (field == null) {
				field = list.Fields.CreateNewField(typeName, title);
				list.Fields.Add(field);
				field = list.Fields[title];
			}
			field.Required = required;
			field.Update();
			return field;
		}

		/// <summary>
		/// Removes all other content types and then sets the content type on a <see cref="SPList"/>.
		/// </summary>
		/// <param name="list">The list to set the content type on.</param>
		/// <param name="contentType">The content type to set.</param>
		/// <returns>True if the list was changes. False if not.</returns>
		public static bool SetContentType(SPList list, SPContentType contentType) {
			if (contentType == null) {
				throw new ArgumentNullException("contentType");
			}
			
			bool addedContentType = AddContentType(list, contentType);
			IEnumerable<SPContentType> contentTypesToDelete = list.ContentTypes
				.Cast<SPContentType>()
				.Where(ct => ct.Id != contentType.Id && ct.Id.Parent != contentType.Id);
			bool anyContentTypesRemoved = RemoveContentTypes(list, contentTypesToDelete);
			bool anythingChanged = addedContentType || anyContentTypesRemoved;
			if (anythingChanged) {
				list.Update();
			}
			return anythingChanged;
		}

		private static bool RemoveContentTypes(SPList list, IEnumerable<SPContentType> contentTypesToDelete) {
			bool changed = false;
			foreach (SPContentType contentTypeToDelete in contentTypesToDelete) {
				_log.Info("removing content type '" + contentTypeToDelete.Name + "' from list '" + list.Title + "'");
				list.ContentTypes.Delete(contentTypeToDelete.Id);
				changed = true;
			}
			return changed;
		}

		/// <summary>
		/// Removes all other content types and then sets the content type on a <see cref="SPList"/>.
		/// </summary>
		/// <param name="list">The list to set the content type on.</param>
		/// <param name="contentTypes">The set of content types to add to the list.</param>
		/// <returns>True if the list was changes. False if not.</returns>
		public static bool SetContentType(SPList list, IEnumerable<SPContentType> contentTypes) {
			if (contentTypes == null) {
				throw new ArgumentNullException("contentTypes");
			}

			bool anyContentTypeAdded = false;

			foreach (SPContentType contentType in contentTypes) {
				bool contentTypeAdded = AddContentType(list, contentType);
				anyContentTypeAdded = anyContentTypeAdded || contentTypeAdded;
			}
			IEnumerable<SPContentType> contentTypesToDelete = list.ContentTypes
				.Cast<SPContentType>()
				.Where(ct => contentTypes.Any(contentType => ct.Id != contentType.Id && ct.Id.Parent != contentType.Id));
			bool anyContentTypesDeleted = RemoveContentTypes(list, contentTypesToDelete);

			bool anyChanged = anyContentTypeAdded || anyContentTypesDeleted;
			if (anyChanged) {
				list.Update();
			}
			return anyChanged;
		}

		private static bool AddContentType(SPList list, SPContentType contentType) {
			SPContentTypeId bestMatch = list.ContentTypes.BestMatch(contentType.Id);
			if (bestMatch.Parent != contentType.Id) {
				_log.Info("adding content type '" + contentType.Name + "' to list '" + list.Title + "'");
				list.ContentTypes.Add(contentType);
				return true;
			}
			return false;
		}

		/// <summary>
		/// Finds or adds a content type to a <see cref="SPList"/>.
		/// </summary>
		/// <param name="list">The list to find or add the content type to.</param>
		/// <param name="contentType">The content type to find or add.</param>
		/// <returns>True, if the list was changed (content type added). False, if the content type already existed.</returns>
		public static bool FindOrAddContentType(SPList list, SPContentType contentType) {
			bool changed = false;
			if (!list.ContentTypesEnabled) {
				list.ContentTypesEnabled = true;
				changed = true;
			}
			SPContentTypeId bestMatch = list.ContentTypes.BestMatch(contentType.Id);
			if (bestMatch.Parent != contentType.Id) {
				_log.Info("adding content type '" + contentType.Name + "' to list '" + list.Title + "'");
				list.ContentTypes.Add(contentType);
				changed = true;
			}
			if (changed) {
				list.Update();
			}
			return changed;
		}

		/// <summary>
		/// Finds a view by title or returns null.
		/// </summary>
		/// <param name="list">The list to find the view on.</param>
		/// <param name="title">The title of the list.</param>
		/// <returns>The view or null if not found.</returns>
		public static SPView FindViewOrDefault(SPList list, string title) {
			return list.Views
				.Cast<SPView>()
				.FirstOrDefault(v => string.Equals(v.Title, title, StringComparison.InvariantCultureIgnoreCase));
		}

		/// <summary>
		/// Find or adds a view to a <see cref="SPList"/>. If the view exists updates the view.
		/// </summary>
		/// <param name="list">The list to find or add a view to.</param>
		/// <param name="viewName">The name of the view.</param>
		/// <param name="viewFields">The list of fields to add to the view</param>
		/// <param name="query"><see cref="SPViewCollection.Add(string,System.Collections.Specialized.StringCollection,string,uint,bool,bool)"/></param>
		/// <param name="rowLimit"><see cref="SPViewCollection.Add(string,System.Collections.Specialized.StringCollection,string,uint,bool,bool)"/></param>
		/// <param name="paged"><see cref="SPViewCollection.Add(string,System.Collections.Specialized.StringCollection,string,uint,bool,bool)"/></param>
		/// <param name="makeViewDefault"><see cref="SPViewCollection.Add(string,System.Collections.Specialized.StringCollection,string,uint,bool,bool)"/></param>
		/// <returns>The new or existing view.</returns>
		public static SPView FindOrAddView(SPList list, string viewName, IEnumerable<string> viewFields, string query, uint rowLimit, bool paged, bool makeViewDefault) {
			return FindOrAddView(list, viewName, viewFields, query, rowLimit, paged, makeViewDefault, SPViewCollection.SPViewType.Html, false);
		}

		/// <summary>
		/// Find or adds a view to a <see cref="SPList"/>. If the view exists updates the view.
		/// </summary>
		/// <param name="list">The list to find or add a view to.</param>
		/// <param name="viewName">The name of the view.</param>
		/// <param name="viewFields">The list of fields to add to the view</param>
		/// <param name="query"><see cref="SPViewCollection.Add(string,System.Collections.Specialized.StringCollection,string,uint,bool,bool)"/></param>
		/// <param name="rowLimit"><see cref="SPViewCollection.Add(string,System.Collections.Specialized.StringCollection,string,uint,bool,bool)"/></param>
		/// <param name="paged"><see cref="SPViewCollection.Add(string,System.Collections.Specialized.StringCollection,string,uint,bool,bool)"/></param>
		/// <param name="makeViewDefault"><see cref="SPViewCollection.Add(string,System.Collections.Specialized.StringCollection,string,uint,bool,bool)"/></param>
		/// <param name="ViewType"><see cref="SPViewCollection.Add(string,System.Collections.Specialized.StringCollection,string,uint,bool,bool)"/></param>
		/// <param name="personalView"><see cref="SPViewCollection.Add(string,System.Collections.Specialized.StringCollection,string,uint,bool,bool)"/></param>
		/// <returns>The new or existing view.</returns>
		public static SPView FindOrAddView(SPList list, string viewName, IEnumerable<string> viewFields, string query, uint rowLimit, bool paged, bool makeViewDefault, SPViewCollection.SPViewType ViewType, bool personalView) {
			SPView view = FindViewOrDefault(list, viewName);
			if (view == null) {
				StringCollection viewFieldsCol = new StringCollection();
				foreach (string viewField in viewFields) {
					SPField field = FindFieldOrDefault(list, viewField);
					if (field == null) {
						throw new SPOrmException("Could not find field '" + viewField + "' in list '" + list.Title + "'");
					}
					viewFieldsCol.Add(field.InternalName);
				}
				view = list.Views.Add(viewName, viewFieldsCol, query, rowLimit, paged, makeViewDefault, ViewType, personalView);
			} else {
				view.ViewFields.DeleteAll();
				foreach (string viewField in viewFields) {
					SPField field = FindFieldOrDefault(list, viewField);
					if (field == null) {
						throw new SPOrmException("Could not find field '" + viewField + "' in list '" + list.Title + "'");
					}
					view.ViewFields.Add(field);
				}
				view.Query = query;
				view.RowLimit = rowLimit;
				view.Paged = paged;
				view.DefaultView = makeViewDefault;
				view.Update();
			}
			return view;
		}

		/// <summary>
		/// adds a role assignment to a <see cref="SPList"/>.
		/// </summary>
		/// <param name="list">The list to add the role assignment to.</param>
		/// <param name="roleAssignment">The role assignment to add.</param>
		/// <param name="roleDefinitions">The role definitions to add. (eg "Full Control", "Read", etc)</param>
		public static void AddRoleAssignment(SPList list, SPRoleAssignment roleAssignment, string[] roleDefinitions) {
			SPWeb site = list.ParentWeb;
			if (!list.HasUniqueRoleAssignments) {
				list.BreakRoleInheritance(false);
				list.Update();
			}
			foreach (string roleDefinition in roleDefinitions) {
				roleAssignment.RoleDefinitionBindings.Add(site.RoleDefinitions[roleDefinition]);
			}
			list.RoleAssignments.Add(roleAssignment);
		}
	}
}
